<?php
# MantisBT - A PHP based bugtracking system

# MantisBT is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# MantisBT is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with MantisBT.  If not, see <http://www.gnu.org/licenses/>.

/**
 * Configuration Parser class.
 * @copyright Copyright 2016 MantisBT Team - mantisbt-dev@lists.sourceforge.net
 * @link http://www.mantisbt.org
 * @package MantisBT
 * @subpackage classes
 */


/**
 * Configuration Parser class
 *
 * Simple PHP code parser for scalar and array types
 *
 * @package MantisBT
 * @subpackage classes
 *
 * @uses Tokenizer
 */
class ConfigParser
{
    /**
     * Define how extra tokens should be handled by parse() method
     */
    const EXTRA_TOKENS_IGNORE = 0;
    const EXTRA_TOKENS_ERROR = 1;

    /**
     * @var Tokenizer $tokens
     */
    protected $tokens;

    /**
     * Parser constructor.
     * @param string $p_code PHP code to parse
     */
    public function __construct($p_code)
    {
        $this->tokens = new Tokenizer($p_code);
    }

    /**
     * Parse the code for a variable assignment.
     * Handles scalar types, and various array types (simple, associative,
     * multi-dimentional)
     * @param integer $p_extra_tokens Define how extra tokens should be handled
     *                             - EXTRA_TOKENS_IGNORE silently ignore any
     *                               extra code given after the first token
     *                             - EXTRA_TOKENS_ERROR (default) throws an
     *                               exception if extra code is found
     * @return mixed variable
     * @throws Exception when there are unexpected or extra tokens
     */
    public function parse($p_extra_tokens = self::EXTRA_TOKENS_ERROR)
    {
        switch ($this->tokens->type()) {
            case T_ARRAY:
                $t_result = $this->process_array();
                break;

            case T_CONSTANT_ENCAPSED_STRING:
            case T_STRING:
            case T_LNUMBER:
            case T_DNUMBER:
            case '-':
            case '+':
                $t_result = $this->process_value();
                break;

            default:
                throw new Exception('Unexpected token "' . $this->tokens->value() . '"');
        }

        # Make sure we have processed all tokens
        if ($p_extra_tokens == self::EXTRA_TOKENS_ERROR && !$this->tokens->is_empty()) {
            throw new Exception('Extra tokens found "' . $this->tokens->get_string() . '":');
        }

        return $t_result;
    }

    /**
     * Check if the passed string is a constant and returns its value if yes,
     * or the string itself if not
     * @param string $p_name String to check.
     * @return mixed|string value of constant $p_name, or $p_name itself
     */
    public static function constant_replace($p_name)
    {
        $t_name = trim($p_name);
        if (is_string($t_name) && defined($t_name)) {
            # we have a constant
            return constant($t_name);
        }
        return $t_name;
    }

    /**
     * Recursively process array declarations.
     * @return array
     * @throws Exception when there's an invalid token
     */
    protected function process_array()
    {
        $t_array = array();
        $t_count = 0;

        $this->tokens->ensure_matches(T_ARRAY);
        $this->tokens->ensure_matches('(');

        # Loop until we reach the end of the array
        while (!$this->tokens->matches(')')) {
            # A comma is required before each element except the first one
            if ($t_count > 0) {
                $this->tokens->ensure_matches(',');
            }

            switch ($this->tokens->type()) {
                # Nested array
                case T_ARRAY:
                    $t_array[] = $this->process_array();
                    break;

                # Value
                case T_CONSTANT_ENCAPSED_STRING:
                case T_STRING:
                case T_LNUMBER:
                case T_DNUMBER:
                    $t_str = $this->process_value();

                    if ($this->tokens->matches(T_DOUBLE_ARROW)) {
                        # key => value
                        $this->tokens->pop();
                        if ($this->tokens->matches(T_ARRAY)) {
                            $t_array[$t_str] = $this->process_array();
                        } else {
                            $t_array[$t_str] = $this->process_value();
                        }
                    } else {
                        # Simple value
                        $t_array[] = $t_str;
                    }
                    break;

                case ')':
                    # Cover the trailing ',' case
                    break;

                default:
                    throw new Exception("Invalid token '" . $this->tokens->value() . "'");
            }

            $t_count++;
        }
        $this->tokens->ensure_matches(')');

        return $t_array;
    }

    /**
     * Process a scalar value.
     * Handles string literals including defined constants
     * @return mixed
     * @throws Exception when there's an unexpected value
     */
    protected function process_value()
    {
        # String literals
        if ($this->tokens->matches(T_STRING)) {
            $t_token = $this->tokens->pop();
            $t_value = $t_token[1];

            # PHP Standard string literals
            switch (strtolower($t_value)) {
                case 'null':
                    return null;
                case 'true':
                    return true;
                case 'false':
                    return false;
            }

            # Defined constants
            $t_value = $this->constant_replace($t_value);
            if ($t_value !== $t_token[1]) {
                return $t_value;
            }

            throw new Exception("Unknown string literal '$t_value'");
        }

        # Strings
        if ($this->tokens->matches(T_CONSTANT_ENCAPSED_STRING)) {
            $t_value = $this->tokens->pop();
            return (string)stripslashes(substr($t_value[1], 1, -1));
        }

        # Numbers
        $t_negate = 1;
        if ($this->tokens->matches('-')) {
            $this->tokens->pop();
            $t_negate = -1;
        }
        if ($this->tokens->matches('+')) {
            $this->tokens->pop();
        }

        # Integers
        if ($this->tokens->matches(T_LNUMBER)) {
            $t_value = $this->tokens->pop();
            return $t_negate * (int)$t_value[1];
        }

        # Floating point
        if ($this->tokens->matches(T_DNUMBER)) {
            $t_value = $this->tokens->pop();
            return $t_negate * (float)$t_value[1];
        }

        # Anything else
        throw new Exception("Unexpected value" . $this->tokens->value());
    }
}
